//@author A0105740R
package rightnow.parser.cmdline;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;

import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import rightnow.parser.DateTimeParsers;
import rightnow.storage.Task;

/**
 * This class provides all necessary methods used by CmdTaskParser.<br>
 * The methods inside this class should be called step by step. After all
 * steps, information related to task description and date time are marked
 * and parsed.
 */
final class CmdTaskParserCore {
	private static final Logger log = 
			LoggerFactory.getLogger(CmdTaskParserCore.class);

	private static final int INDEX_NOT_FOUND = -1;
	
	private static final int ONE_ELEM_DIRECT_AFTER = 1;
	private static final int ALL_ELEM_DIRECT_AFTER = 0;
	
	private static final String IDTF_AT_REGEX = "\\b[Aa][Tt]\\b";
	private static final String IDTF_START_REGEX = "\\b[Ss]tarting\\b";
	private static final String IDTF_BY_REGEX = "\\b[Bb][Yy]\\b";
	private static final String IDTF_ON_REGEX = "\\b[Oo][Nn]\\b";
	
	private static final TokenType[] DATE_IDTF_ARR =
			new TokenType[] 
				{TokenType.IDTF_ON, TokenType.IDTF_BY, TokenType.IDTF_START};
	
	private static final String DATE_MODF_REGEX = "\\b([Ll]ast)|([Nn]ext)\\b";
	private static final String DATE_MODF_LAST_REGEX = "[Ll]ast";
	private static final String DATE_MODF_NEXT_REGEX = "[Nn]ext";

	private static final int MAX_NUM_UNDET_DATE = 2;

	private int positionAtStartTime = INDEX_NOT_FOUND;
	private int positionByEndTime = INDEX_NOT_FOUND;
	private boolean isStartDateDeterminedByIdtf = false;
	private int positionStartDate = INDEX_NOT_FOUND;
	private boolean isEndDateDeterminedByIdtf = false;
	private int positionEndDate = INDEX_NOT_FOUND;
	
	private int positionUndetDate1 = INDEX_NOT_FOUND;
	private int positionUndetDate2 = INDEX_NOT_FOUND;
	
	private boolean isStartDateParsed = false;
	private boolean isEndDateParsed = false;
	private boolean isStartDateAdjustable = false;
	private boolean isEndDateAdjustable = false;
	private LocalTime startTime = null;
	private LocalTime endTime = null;
	private LocalDate startDate = null;
	private LocalDate endDate = null;
	
	private LinkedList<Token> tokenList;

	/**
	 * 
	 * @param tokenList != null
	 */
	CmdTaskParserCore(LinkedList<Token> tokenList) {
		this.tokenList = tokenList;
	}
	
	private void markIdtf() {
		Tokenizer.tokenizeAll(tokenList, IDTF_AT_REGEX, TokenType.IDTF_AT);
		Tokenizer.tokenizeAll(tokenList, IDTF_START_REGEX, TokenType.IDTF_START);
		Tokenizer.tokenizeAll(tokenList, IDTF_BY_REGEX, TokenType.IDTF_BY);
		Tokenizer.tokenizeAll(tokenList, IDTF_ON_REGEX, TokenType.IDTF_ON);
	}
	
	private void markTime() {
		Tokenizer.tokenizeAllDirectAfter(tokenList, 
				TokenType.IDTF_AT, DateTimeParsers.TIME_REGEX, 
				TokenType.STIME, ONE_ELEM_DIRECT_AFTER);
		Tokenizer.tokenizeAllDirectAfter(tokenList,
				TokenType.IDTF_BY, DateTimeParsers.TIME_REGEX, 
				TokenType.ETIME, ONE_ELEM_DIRECT_AFTER);
	}
	
	private void markDate() {
		Tokenizer.tokenizeAllDirectAfter(tokenList, 
				DATE_IDTF_ARR, DateTimeParsers.APPROX_STD_DATE_REGEX, 
				TokenType.STD_DATE, ONE_ELEM_DIRECT_AFTER);
		Tokenizer.tokenizeAllDirectAfter(tokenList, 
				DATE_IDTF_ARR, DateTimeParsers.WEEKDAY_REGEX, 
				TokenType.WEEKDAY, ONE_ELEM_DIRECT_AFTER);
		Tokenizer.tokenizeAllDirectAfter(tokenList,
				DATE_IDTF_ARR, DATE_MODF_REGEX,
				TokenType.DATE_MODF, ALL_ELEM_DIRECT_AFTER);
		Tokenizer.tokenizeAllDirectAfter(tokenList, 
				TokenType.DATE_MODF, DateTimeParsers.WEEKDAY_REGEX, 
				TokenType.WEEKDAY, ONE_ELEM_DIRECT_AFTER);
		Tokenizer.tokenizeAll(tokenList, DateTimeParsers.TMR_YESTERDAY_REGEX, 
				TokenType.DISCRETE_DATE);
	}
	
	/**
	 * The first method to call
	 */
	void markIdtfTimeDate() {
		markIdtf();
		markTime();
		markDate();
	}
	
	/**
	 * <b>pre:</b>	markIdtfTimeDate() is called
	 */
	void elimExtraIdtf() {
		ListIterator<Token> iter = tokenList.listIterator();
		while (iter.hasNext()) {
			Token idtfToken = iter.next();
			if (iter.hasNext()) {
				TokenType judgingTokenType = iter.next().getTokenType();
				switch (idtfToken.getTokenType()) {
				case IDTF_AT:
					if (judgingTokenType != TokenType.STIME) {
						idtfToken.setTokenType(TokenType.TEXT);
						iter.previous();
					}
					break;
				case IDTF_BY:
					if (judgingTokenType!= TokenType.ETIME 
					&& !isDateToken(judgingTokenType)) {
						idtfToken.setTokenType(TokenType.TEXT);
						iter.previous();
					}
					break;
				case IDTF_ON:
					if (!isDateToken(judgingTokenType)) {
						idtfToken.setTokenType(TokenType.TEXT);
						iter.previous();
					}
					break;
				case IDTF_START:
					if (!isDateToken(judgingTokenType)) {
						idtfToken.setTokenType(TokenType.TEXT);
						iter.previous();
					}
					break;
				case DISCRETE_DATE:
					if (judgingTokenType == TokenType.TEXT 
					|| judgingTokenType == TokenType.UNDET) {
						idtfToken.setTokenType(TokenType.TEXT);
						iter.previous();
					}
				default:
					iter.previous();
					break;
				}
			}
			else {
				switch (idtfToken.getTokenType()) {
				case IDTF_AT:
				case IDTF_START:
				case IDTF_BY:
				case IDTF_ON:
					idtfToken.setTokenType(TokenType.TEXT);
					break;
				default:
					break;
				}
			}
		}
	}
	
	/**
	 * check keywords 'at', 'starting', 'by'<br>
	 * <b>pre:</b> elimExtraIdtf() is called.<br>
	 * <b>post:</b> positionAtStartTime, positionByEndTime, 
	 * 		positionByEndDate are marked.
	 * @return true if the token list has valid number of the keywords
	 */
	boolean hasValidNumIdtf() {
		ListIterator<Token> iter = tokenList.listIterator();
		while (iter.hasNext()) {
			switch (iter.next().getTokenType()) {
			case IDTF_AT:
				if (positionAtStartTime == INDEX_NOT_FOUND) {
					positionAtStartTime = iter.previousIndex();
				}
				else {
					return false;
				}
				break;
			case IDTF_BY:
				TokenType timeOrDateTokenType = iter.next().getTokenType();
				if (timeOrDateTokenType == TokenType.ETIME) {
					if (positionByEndTime == INDEX_NOT_FOUND) {
						positionByEndTime = iter.previousIndex() - 1;
					}
					else {
						return false;
					}
				}			
				else if (isDateToken(timeOrDateTokenType)) {
					if (positionEndDate == INDEX_NOT_FOUND) {
						ListIterator<Token> tempIter = 
								tokenList.listIterator(iter.previousIndex());
						isEndDateDeterminedByIdtf = true;
						positionEndDate = determineRealDatePosition(tempIter);
					}
					else {
						return false;
					}
				}
				break;
			case IDTF_START:
				TokenType dateTokenType = iter.next().getTokenType();
				if (isDateToken(dateTokenType)) {
					if (positionStartDate == INDEX_NOT_FOUND)  {
						ListIterator<Token> tempIter = 
								tokenList.listIterator(iter.previousIndex());
						isStartDateDeterminedByIdtf = true;
						positionStartDate = determineRealDatePosition(tempIter);
					}
					else {
						return false;
					}
				}
				break;
			default:
				break;
			}
		}
		return true;
	}
	
	/**
	 * <b>pre:</b> hasValidNumIdtf() is true
	 * @return true if keywords 'at' and 'starting' are before 'by'
	 */
	boolean isStartIdtfBeforeEndIdtf() {
		if (positionAtStartTime != INDEX_NOT_FOUND) {
			if (positionByEndTime != INDEX_NOT_FOUND) {
				if (positionAtStartTime >= positionByEndTime) {
					return false;
				}
			}
			if (positionEndDate != INDEX_NOT_FOUND) {
				if (positionAtStartTime >= positionEndDate) {
					return false;
				}
			}
		}
		
		if (positionStartDate != INDEX_NOT_FOUND) {
			if (positionByEndTime != INDEX_NOT_FOUND) {
				if (positionStartDate >= positionByEndTime) {
					return false;
				}
			}
			if (positionEndDate != INDEX_NOT_FOUND) {
				if (positionStartDate >= positionEndDate) {
					return false;
				}
			}
		}
		
		return true;
	}
	
	/**
	 * <b>pre:</b> hasValidNumIdtf() is true
	 */
	void determineTime() {
		if (positionAtStartTime != INDEX_NOT_FOUND) {
			Token nextToken = tokenList.get(positionAtStartTime + 1);
			if (nextToken.getTokenType() == TokenType.STIME) {
				startTime = DateTimeParsers.parseTime(
						nextToken.getTokenString());
			}
		}
		if (positionByEndTime != INDEX_NOT_FOUND) {
			Token nextToken = tokenList.get(positionByEndTime + 1);
			if (nextToken.getTokenType() == TokenType.ETIME) {
				endTime = DateTimeParsers.parseTime(
						nextToken.getTokenString());
			}
		}
	}
	
	/**
	 * <b>pre:</b> hasValidNumIdtf() is true<br>
	 * <b>post:</b> the position of implicit start and end dates are determined
	 * @return true if the token list has valid number of 'on' labeled dates
	 */
	boolean hasValidNumUndetDates() {
		int count = 0;
		ListIterator<Token> iter = tokenList.listIterator();
		while (iter.hasNext()) {
			if (isRealDateToken(iter.next().getTokenType()) &&
					!isDeterminedDatePosition(iter.previousIndex())) {
				if (count == 0) {
					positionUndetDate1 = iter.previousIndex();
				}
				else if (count == 1) {
					positionUndetDate2 = iter.previousIndex();
				}
				else if (count == MAX_NUM_UNDET_DATE){
					return false;
				}
				count++;
			}
		}

		int undetDateLimit = determineUndetDateLimit();
		
		switch (undetDateLimit) {
		case 0:
			return positionUndetDate1 == INDEX_NOT_FOUND;
		case 1:
			if (positionUndetDate2 != INDEX_NOT_FOUND) {
				return false;
			}
			else {
				if (positionUndetDate1 != INDEX_NOT_FOUND) {
					if (positionStartDate == INDEX_NOT_FOUND) {
						/*
						 * positionStartDate is not found 
						 * but positionEndDate is found
						 * it should not conflict with endDate
						 * so positionAtStartTime must exist
						 */
						return positionAtStartTime != INDEX_NOT_FOUND;
					}
					else {
						/*
						 *  positionStartDate is found 
						 *  but positionEndDate is not found
						 */
						return positionByEndTime != INDEX_NOT_FOUND;
					}
				}
				else {
					return true;
				}
			}
		case MAX_NUM_UNDET_DATE:
			if (positionUndetDate2 != INDEX_NOT_FOUND) {
				return (positionAtStartTime != INDEX_NOT_FOUND) 
						&& (positionByEndTime != INDEX_NOT_FOUND);
			}
			else {
				return true;
			}
		default:
			log.error("var undetDateLimit < 0 or exceeds MAX_NUM_UNDET_DATE");
			assert false;
			return false;
		}
	}
	
	/**
	 * <b>pre:</b> hasValidNumUndetDates() is true<br>
	 * <b>post:</b> the meaning of implicit dates are determined
	 * @return
	 */
	boolean isValidPositionUndetDate() {
		int undetDateLimit = determineUndetDateLimit();
		
		if (undetDateLimit == 1) {
			if (positionUndetDate1 != INDEX_NOT_FOUND) {
				if (positionStartDate == INDEX_NOT_FOUND) {		
					positionStartDate = positionUndetDate1;
				}
				else {
					positionEndDate = positionUndetDate1;
				}
			}
		}
		else if (undetDateLimit == MAX_NUM_UNDET_DATE) {
			if (positionUndetDate2 != INDEX_NOT_FOUND) {
				/*
				 * if (positionAtStartTime != INDEX_NOT_FOUND &&
				 *		positionByEndTime != INDEX_NOT_FOUND) {
				 * This condition is already tested
				 * 
				 * positionAtStartTime < positionByEndTime is confirmed
				 */
				if (positionUndetDate2 < positionByEndTime) {
					return false;
				}
				else {
					positionStartDate = positionUndetDate1;
					positionEndDate = positionUndetDate2;
				}
			}
			else {
				if (positionUndetDate1 != INDEX_NOT_FOUND) {
					if ((positionAtStartTime != INDEX_NOT_FOUND) &&
							(positionByEndTime != INDEX_NOT_FOUND)) {
						// positionAtStartTime < positionByEndTime is confirmed
						if (positionUndetDate1 < positionByEndTime) {
							positionStartDate = positionUndetDate1;
							isEndDateAdjustable = true;
						}
						else {
							positionEndDate = positionUndetDate1;
							isStartDateAdjustable = true;
						}
					}
					else if ((positionAtStartTime != INDEX_NOT_FOUND) &&
							(positionByEndTime == INDEX_NOT_FOUND)) {
						positionStartDate = positionUndetDate1;
					}
					else if ((positionByEndTime != INDEX_NOT_FOUND) &&
							(positionAtStartTime == INDEX_NOT_FOUND)) {
						positionEndDate = positionUndetDate1;
					}
					else {
						positionStartDate = positionEndDate = positionUndetDate1;
					}
				}
			}
		}
		return true;
	}
	
	/**
	 * <b>pre:</b> isValidPositionUndetDate() is true
	 */
	void determineDate() {
		if (positionStartDate != INDEX_NOT_FOUND) {
			isStartDateParsed = true;
			startDate = parseDateToken(tokenList.get(positionStartDate));
		}
		if (positionEndDate != INDEX_NOT_FOUND) {
			isEndDateParsed = true;
			endDate = parseDateToken(tokenList.get(positionEndDate));
		}
	}
	
	/**
	 * <b>pre:</b> dertermineDate()
	 * @return true if no parsing error occurred in determineDate()
	 */
	boolean isParsingSuccessful() {
		if (isStartDateParsed && startDate == null) {
			return false;
		}
		if (isEndDateParsed && endDate == null) {
			return false;
		}
		return true;
	}

	/**
	 * <b>pre:</b> isParsingSuccessful() is true
	 */
	void adjustDate() {
		if (positionStartDate != INDEX_NOT_FOUND) {
			// adjust "next", function with date index
			startDate = adjustDate(startDate, positionStartDate);
		}
		if (positionEndDate == positionStartDate) {
			endDate = startDate;
		}
		else if(positionEndDate != INDEX_NOT_FOUND) {
			// call the same function
			endDate = adjustDate(endDate, positionEndDate);
		}
	}

	/**
	 * Auto fill the null startTime, startDate, endTime, endDate<br>
	 * <b>pre:</b> adjustDate()
	 */
	void determineImplicitDateAndTime() {
		if (startDate != null && startTime == null) {
			startTime = new LocalTime(0, 0);
		}
		if (endDate != null && endTime == null) {
			endTime = new LocalTime(23, 59);
		}
		if (startTime != null && startDate == null) {
			if (endDate != null && !isEndDateDeterminedByIdtf) {
				startDate = endDate;
			}
			else {
				startDate = new LocalDate();
			}	
		}
		if (endTime != null && endDate == null) {
			if (startDate != null && !isStartDateDeterminedByIdtf) {
				endDate = startDate;
			}
			else {
				endDate = new LocalDate();
			}
		}
	}
	
	/**
	 * <p>This function checks whether startDateTime exceeds endDateTime.<br>
	 * If startDate or endDate adjustable, adjust it.</p>
	 * <b>pre:</b> determineImplicitDateAndTime()
	 * @return true if start time is before end time
	 */
	boolean isValidStartEndDateTime() {
		if (startDate != null && endDate != null) {
			LocalDateTime startDateTime = new LocalDateTime(startDate.getYear(),
					startDate.getMonthOfYear(), startDate.getDayOfMonth(),
					startTime.getHourOfDay(), startTime.getMinuteOfHour());
			LocalDateTime endDateTime = new LocalDateTime(endDate.getYear(),
					endDate.getMonthOfYear(), endDate.getDayOfMonth(),
					endTime.getHourOfDay(), endTime.getMinuteOfHour());
			if (startDateTime.compareTo(endDateTime) >= 0) {
				if (isStartDateAdjustable) {
					if (startDateTime.plusDays(-1).compareTo(endDateTime) < 0) {
						startDate = startDate.plusDays(-1);
					}
					else {
						return false;
					}				
				}
				else if (isEndDateAdjustable) {
					if (startDateTime.compareTo(endDateTime.plusDays(1)) < 0) {
						endDate = endDate.plusDays(1);
					}
					else {
						return false;
					}
				}
				else {
					return false;
				}
			}
		}
		return true;
	}
	
	Task getResult() {
		Task newTask = new Task(getText());
		newTask.setStartTime(startTime);
		newTask.setEndTime(endTime);
		newTask.setStartDate(startDate);
		newTask.setEndDate(endDate);
		return newTask;
	}
	
	/**
	 * This method is used by CmdAddParser to check whether all identifiers
	 * are at the back
	 * @return
	 */
	public boolean isIdtfAllAtBack() {
		Iterator<Token> iter = tokenList.iterator();
		TokenType currentType;
		while (iter.hasNext()) {
			currentType = iter.next().getTokenType();
			if (currentType != TokenType.TEXT && 
					currentType != TokenType.UNDET) {
				break;
			}
		}
		while (iter.hasNext()) {
			currentType = iter.next().getTokenType();
			if (currentType == TokenType.TEXT || 
					currentType == TokenType.UNDET) {
				return false;
			}
		}
		return true;
	}
	
	public boolean hasExtraText() {
		boolean hasText = false;
		for (Token token: tokenList) {
			if (token.getTokenType() == TokenType.TEXT) {
				if (!hasText) {
					hasText = true;
				}
				else {
					return true;
				}
			}
		}
		return false;
	}
	
	public boolean hasText() {
		for (Token token: tokenList) {
			if (token.getTokenType() == TokenType.TEXT) {
				return true;
			}
		}
		return false;
	}

	public void markUndtAsText() {
		for (Token token: tokenList) {
			if (token.getTokenType() == TokenType.UNDET) {
				token.setTokenType(TokenType.TEXT);
			}
		}
	}

	private int determineUndetDateLimit() {
		int limit = MAX_NUM_UNDET_DATE;
		if (positionStartDate != INDEX_NOT_FOUND) {
			limit--;
		}
		if (positionEndDate != INDEX_NOT_FOUND) {
			limit--;
		}
		return limit;
	}

	private int determineRealDatePosition(ListIterator<Token> iter) {
		while (iter.hasNext()) {
			if (isRealDateToken(iter.next().getTokenType())) {
				return iter.previousIndex();
			}
		}
		return INDEX_NOT_FOUND;
	}

	private String getText() {
		StringBuilder sBd = new StringBuilder();
		for (Token token:tokenList) {
			if (token.getTokenType() == TokenType.TEXT
					|| token.getTokenType() == TokenType.UNDET) {
				sBd.append(token.getTokenString() + ' ');
			}
		}
		sBd.deleteCharAt(sBd.length() - 1);
		return sBd.toString();
	}

	private boolean isDateToken(TokenType tokenType) {
		switch (tokenType) {
		case STD_DATE:
		case WEEKDAY:
		case DISCRETE_DATE:
		case DATE_MODF:
			return true;
		default:
			return false;
		}
	}
	
	private boolean isDeterminedDatePosition(int index) {
		return (index == positionStartDate || index == positionEndDate)? 
				true: false;
	}

	private boolean isRealDateToken(TokenType tokenType) {
		switch (tokenType) {
		case STD_DATE:
		case WEEKDAY:
		case DISCRETE_DATE:
			return true;
		default:
			return false;
		}
	}
	
	/**
	 * 
	 * @param dateToken
	 * @return null if parsing is not successful
	 */
	private LocalDate parseDateToken(Token dateToken) {
		if (dateToken.getTokenType() == TokenType.STD_DATE) {
			return DateTimeParsers.parseStdDate(dateToken.getTokenString());
		}
		else if (dateToken.getTokenType() == TokenType.WEEKDAY) {
			return DateTimeParsers.parseWeekDay(dateToken.getTokenString());
		}
		else if (dateToken.getTokenType() == TokenType.DISCRETE_DATE) {
			return DateTimeParsers.parseTmrYesterday(dateToken.getTokenString());
		}
		return null;
	}
	
	private LocalDate adjustDate(LocalDate date, int index) {
		ListIterator<Token> iter = this.tokenList.listIterator(index);
		while (iter.hasPrevious()) {
			Token prevToken = iter.previous();
			if (prevToken.getTokenType() == TokenType.DATE_MODF) {
				if (prevToken.getTokenString().matches(DATE_MODF_LAST_REGEX)) {
					log.trace("date plusWeeks(-1)");
					date = date.plusWeeks(-1);
				}
				else if (prevToken.getTokenString().
						matches(DATE_MODF_NEXT_REGEX)) {
					log.trace("date plusWeeks(1)");
					date = date.plusWeeks(1);
				}
			}
			else {
				break;
			}
		}
		return date;
	}
}
